/*
 * Created on Nov 14, 2004
 *
 * TODO To change the template for this generated file go to
 * Window - Preferences - Java - Code Style - Code Templates
 */
package org.mtdownload.download;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.RandomAccessFile;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;

import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPCommand;
import org.apache.commons.net.ftp.FTPReply;
import org.mtdownload.utils.PreferencesManager;

/**
 * This is a download thread. Is responsible for downloading a part (or all) of a file.
 * It's the main thread started. If possible (and requested) it will spawn other threads 
 * @author sergiu
 */
public class DownloadThread implements Runnable,Serializable {
	
	/**
	 * Comment for <code>serialVersionUID</code>
	 */
	private static final long serialVersionUID = 3978429109103702326L;
	private long startingPosition=0;
	protected URL url;
	protected DownloadTask task;
	protected DownloadManager manager;	
	protected long downloadFileLength=0;
	protected boolean cancel=false;
	protected boolean stopped=false;//a flag that gets setted to true only by another thread
	protected File destinationFile=null;
	protected Vector<DownloadThread> threads=null;	
	private long currentPosition=0;
	private long endPosition=0;
	protected BytesCounter counter=null;
	public static int BLOCK_SIZE=512;
	private String name="";
	private DownloadMasterThread masterThread=null;
	private int index=0;
	long measuredBytes=0;
	protected boolean completed=false;//a flag that will tell us if the thread is completed	
	protected static PrintStream fromServerStream=System.out;
	protected static PrintStream infoStream=System.out;
	protected static PrintStream fromClientStream=System.out;
	protected static PrintStream errorStream=System.err;
	
	public DownloadThread(DownloadTask task,long startingPosition){
		setTask(task);
		setStartingPosition(startingPosition);
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Runnable#run()
	 */
	public void run() {
		try{
			//if we're stopped for whatever reason ... don't do anything
			if(isStopped()){
				System.out.println(this + " is stopped. can't start downloading.");
				return;
			}
			measuredBytes=0;
			URL url=new URL(task.getUrl());
			String protocol=url.getProtocol();
			if(protocol.equalsIgnoreCase("http") || protocol.equalsIgnoreCase("https")){
				startHTTPDownload(task,url);
			}
			if(protocol.equalsIgnoreCase("ftp")){
				startFTPDownload(task,url);
			}			
		}catch(Exception exc){
			exc.printStackTrace();
		}	
	}
			
	protected RandomAccessFile createFile(File file,long length){
		RandomAccessFile out=null;
		if(file.exists()){
			//we have to resume here
			try{
				out=new RandomAccessFile(file,"rwd");				
			}catch(Exception exc){
				exc.printStackTrace();
			}
		}else{
			//we create a new file
			try{
				file.getParentFile().mkdirs();
				out=new RandomAccessFile(file,"rwd");
				//Here's what I've done:
				//create a new file, move the pointer to the determined file length-1 position in the file,
				//and write one byte there. Very fast :).
				if(length>0){
					//only if we have a positive length. If the length of the download cannot be determined,
					//(usually becuase of bad servers implementations, not giving the headers, etc.)
					//the don't do anything.
					FileChannel fileCh=out.getChannel();
					fileCh.position(length-1);
					byte[] data=new byte[1];
					data[0]=0;
					ByteBuffer buf=ByteBuffer.wrap(data);
					fileCh.write(buf);
				}
			}catch(Exception exc){
				exc.printStackTrace();
			}			
		}		
		return out;
	}
	
	protected HttpURLConnection getHTTPConnection(URL url)throws Exception{
		HttpURLConnection  connection=null;
		if(url.getProtocol().equalsIgnoreCase("https")){
			HttpsURLConnection ssl_con=(HttpsURLConnection)url.openConnection();			
			ssl_con.setHostnameVerifier(new HostnameVerifier(){
				public boolean verify(String host,SSLSession session){
					return true;
				}
			});
			connection=ssl_con;
		}else{
			connection=(HttpURLConnection)url.openConnection();	
		}
		return connection;
	}
	
	protected void startHTTPDownload(final DownloadTask task,URL url) throws Exception{
		HttpURLConnection  connection=getHTTPConnection(url);
		
		connection.addRequestProperty("User-Agent","MTDownload 0.1");
		if(task.getReferer()!=null && task.getReferer().length()>0){
			connection.addRequestProperty("Referer",task.getReferer());
		}
		infoStream.println(getName()+" starts at "+getCurrentPosition());
		connection.addRequestProperty("Range","bytes="+getCurrentPosition()+"-");		
		connection.connect();
		int responseCode=connection.getResponseCode();
		
		if(responseCode!=HttpURLConnection.HTTP_PARTIAL&& 
		    responseCode!=HttpURLConnection.HTTP_ACCEPTED && 
			responseCode!=HttpURLConnection.HTTP_OK){
			errorStream.println("Error connecting to server");
			errorStream.println("Reponse message:"+connection.getResponseMessage());
			connection.disconnect();			
			return;
		}
		
		String destination=task.getDestinationFileName();
		infoStream.println("destination: "+destination);
		destinationFile=new File(destination);
        infoStream.println("Headers for thread: "+getName());
		Map<String,List<String>> hmap=connection.getHeaderFields();
        Set<String> hset=hmap.keySet();
        Iterator<String> it=hset.iterator();
        while(it.hasNext()){
                Object key=it.next();
                fromServerStream.println(key+" "+hmap.get(key));
        }
		
		RandomAccessFile out=createFile(destinationFile,downloadFileLength);
        InputStream in=connection.getInputStream();
		
        out.seek(getCurrentPosition());
		try {
			downloadBytes(out,in);	
		} catch (Exception e) {
			e.printStackTrace();
		}		
		connection.disconnect();
		in.close();
		out.close();
		infoStream.println(getName()+" has finished at "+getCurrentPosition());		
		masterThread.threadCompleted(this);		
	}

	/**
	 * This method is actually getting the bytes, and writing them to the file
	 * @param out the file where we're putting the bytes
	 * @param in the stream from where we're reading
	 * @throws Exception the Exception that is thrown if an error occurs
	 */
	protected void downloadBytes(RandomAccessFile out,InputStream in)throws Exception{
		byte[] data=new byte[BLOCK_SIZE];
		int read=-1;		
		fromClientStream.println(this +" is downloading bytes");
		while((read=in.read(data))>=0){
			//before write ... check to see if we're not overlapping with other threads			
			for(DownloadThread t:threads){
				if(t==this) continue;
				if(t.isCancelled()) continue;
				if(startingPosition<t.getStartingPosition()){
					//we have an thread that we might overlap, and that thread is actually downloading
					//if that thread didn't got to download 1 byte when we got here ... we're gonna have to
					//continue like the thread is not there
					if((currentPosition+read)>=t.getStartingPosition()){
						System.out.println(this+" is overlapping "+t);
						if(t.getCurrentPosition()-t.getStartingPosition()<=0){
							//if the thread didn't managed to get 1 byte, cancel it
							t.setStopped(true);
							System.out.println("cancelling "+t);
						}else{
							//oki ...we ARE overlapping
							//get out fast, but not before writing whatever bytes we have left						
							if((t.getStartingPosition()-currentPosition)>0){
								out.write(data,0,(int)(t.getStartingPosition()-currentPosition));
								addCompletedBytes((int)(t.getStartingPosition()-currentPosition));
								updateCurrentPosition((int)(t.getStartingPosition()-currentPosition));
							}							
							System.out.println( t + " downloaded "+(t.getCurrentPosition()-t.getStartingPosition())+" bytes, returning");
							return;							
						}
					}
				}
			}		 
			out.write(data,0,read);
			addCompletedBytes(read);			
			updateCurrentPosition(read);
			if(isCancelled()){
				break;
			}
		}
	}
	
	protected synchronized void updateCurrentPosition(int readBytes){
		currentPosition+=readBytes;		
	}
	
	protected void setBytesCounter(BytesCounter counter){
		this.counter=counter;
	}
	
	
	public void addCompletedBytes(int count){
		//System.out.println("completed bytes:"+count+" by "+getName());		
		counter.updateCompletedBytes(count);		
		//manager.notifyListeners(task);
	}
	
	protected void startFTPDownload(DownloadTask task,URL url){
		FTPClient ftp=new FTPClient();
		//could not connect to server. aborting
		//ftp.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out)));
		try{
			int port=(url.getPort()!=-1?url.getPort():url.getDefaultPort());
			ftp.connect(url.getHost(),port);
			fromClientStream.println("connected to "+url.getHost());
			fromServerStream.println(ftp.getReplyString());
			int reply=ftp.getReplyCode();
			if(!FTPReply.isPositiveCompletion(reply)){
				ftp.disconnect();
				errorStream.println("ftp server refused connection");
				return;
			}
		}catch(IOException exc){
			if(ftp.isConnected()){
				try{
					ftp.disconnect();
				}catch(IOException ignored){/*ignored*/}
				
			}
			errorStream.println("could not connect to server. aborting");
			exc.printStackTrace();
			return;
		}
		String username=task.getUsername();
		String password=task.getPassword();
		if(username==null){
			//set the username to anonymous, and an e-mail adress as the password
			username="anonymous";
			password=PreferencesManager.getAnonymousFTPPassword();
		}	
		try{
			if(!ftp.login(username,password)){
				ftp.logout();
				errorStream.println("Error logging in. Aborting.");
				return;
			}			
			infoStream.println("remote system is :" + ftp.getSystemName());
			int reply=ftp.sendCommand(FTPCommand.REST,new Long(getCurrentPosition()).toString());
			infoStream.println("rest reply is "+reply);
			String ftpFileType=PreferencesManager.getFTPTransferType();
			if(ftpFileType.equals(PreferencesManager.FTP_TYPE_ASCII)){
				ftp.setFileType(FTP.ASCII_FILE_TYPE);		
			}else{
				ftp.setFileType(FTP.BINARY_FILE_TYPE);
			}
			ftp.enterLocalPassiveMode();			
			String destination=task.getDestinationFileName();
			infoStream.println("destination: "+destination);
			destinationFile=new File(destination);			
			RandomAccessFile out=createFile(destinationFile,downloadFileLength);
			InputStream in=ftp.retrieveFileStream(url.getPath());	
			//System.out.println("input stream is:"+in);
			if(in==null){
				fromServerStream.println("reply code is "+ftp.getReplyCode());
				fromServerStream.println("reply code is "+ftp.getReplyString());
				return;
			}
			out.seek(getCurrentPosition());
			try {
				downloadBytes(out,in);				
			} catch (Exception e) {
				//ignored
			}
			out.close();			
			infoStream.println(getName()+" has finished at "+getCurrentPosition());
			masterThread.threadCompleted(this);
		}catch(Exception exc){
			exc.printStackTrace();
		}finally{
			if(ftp.isConnected()){
				try{
					ftp.disconnect();
				}catch(IOException ignored){/*ignored*/}				
			}
		}
	}

	/**
	 * @return Returns the task.
	 */
	public DownloadTask getTask() {
		return task;
	}
	/**
	 * @param task The task to set.
	 */
	public void setTask(DownloadTask task) {
		this.task = task;
	}
	/**
	 * @param manager The manager to set.
	 */
	public void setManager(DownloadManager manager) {
		this.manager = manager;
	}
	/**
	 * @return Returns the cancel.
	 */
	public boolean isCancelled() {
		return cancel;
	}
	/**
	 * @param cancel The cancel to set.
	 */
	public void setCancelled(boolean cancel) {
		this.cancel = cancel;
	}
	/**
	 * @return Returns the startingPosition.
	 */
	public long getStartingPosition() {
		return startingPosition;
	}
	/**
	 * @param startingPosition The startingPosition to set.
	 */
	public void setStartingPosition(long startingPosition) {
		this.startingPosition = startingPosition;
		currentPosition=startingPosition;		
	}	
	public long getCurrentPosition(){
		return currentPosition;
	}
	
	/**
	 * @param threads The threads to set.
	 */
	protected void setThreads(Vector<DownloadThread> threads) {
		this.threads = threads;
	}
	/**
	 * @return Returns the threads.
	 */
	public Vector<DownloadThread> getThreads() {
		return threads;
	}
	/**
	 * @return Returns the name.
	 */
	public String getName() {
		return name;
	}
	/**
	 * @param name The name to set.
	 */
	public void setName(String name) {
		this.name = name;
	}
	/**
	 * @param masterThread The masterThread to set.
	 */
	protected void setMasterThread(DownloadMasterThread masterThread) {
		this.masterThread = masterThread;
	}
	
	/**
	 * @return Returns the endPosition.
	 */
	public long getEndPosition() {
		return endPosition;
	}
	/**
	 * @param endPosition The endPosition to set.
	 */
	public void setEndPosition(long endPosition) {
		this.endPosition = endPosition;
	}
	
	public void setIndex(int index){
		this.index=index;
	}
	
	public int getIndex(){
		return index;
	}
	
	/**
	 * Returns this thread's speed. Actually, it returns the number of bytes downloaded since the 
	 * last interogation. It should only be called once a second.
	 * @return
	 */
	public long getThreadSpeed(){
		long completedBytes=currentPosition-startingPosition;
		long diff=completedBytes-measuredBytes;
		measuredBytes=completedBytes;
		return diff;
	}

	/**
	 * @return Returns the completed.
	 */
	public boolean isCompleted() {
		return completed;
	}

	/**
	 * @param completed The completed to set.
	 */
	public void setCompleted(boolean completed) {
		this.completed = completed;
	}
	
	public String toString(){
		return getName();
	}

	/**
	 * @return Returns the stopped.
	 */
	public boolean isStopped() {
		return stopped;
	}

	/**
	 * @param stopped The stopped to set.
	 */
	public void setStopped(boolean stopped) {
		this.stopped = stopped;
	}
}
